﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;

namespace Neitzel.Irc.Client
{
    /// <summary>
    /// IrcClient with all functions required to connect to a IRC ServerConfig
    /// </summary>
    public class IrcClient : IDisposable, INotifyPropertyChanged
    {
        #region Fields

        /// <summary>
        /// Tracesource for all log messages.
        /// </summary>
        private static readonly TraceSource Logger = new TraceSource(IrcConstants.TraceSourceName);

        /// <summary>
        /// Flag to signal if the object is disposed already!
        /// </summary>
        private bool _disposed;

        #endregion
        
        #region Lifetime

        /// <summary>
        /// Initializes a new instance of the IrcClient class.
        /// </summary>
        /// <remarks>
        /// When using this standard constructor, we are subscribed to all events.
        /// </remarks>
        public IrcClient()
            : this(IrcClientEventTypes.AllEvents)
        {
        }

        /// <summary>
        /// Initializes a new instance of the IrcClient class.
        /// </summary>
        /// <param name="eventsTypes">Types of events that should be handled automatically.</param>
        public IrcClient(IrcClientEventTypes eventsTypes)
        {
            // Initialize COnfiguration
            _configuration = new ClientConfig();
            
            // Initialize the Channels and MessageStores
            Channels = new Dictionary<string, IrcChannel>();
            MessageStores = new Dictionary<string, MessageStore>();

            // Register for requested events
            JoinEvents(eventsTypes);

            // Get a first MessageStore for the StatusWindow
            StatusMessages = new MessageStore();
            MessageStores.Add(string.Empty, StatusMessages);
        }

        #endregion

        #region Events

        /// <summary>
        /// Event that a new Channel was created.
        /// </summary>
        public event EventHandler<ChannelEventArgs> NewChannelCreated;

        #region IRC message related events

        /// <summary>
        /// Event that a ping message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> PingMessageReceived;

        /// <summary>
        /// Event that a notice message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> NoticeMessageReceived;

        /// <summary>
        /// Event that an "error no such nick" message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> ErrNoSuchNickMessageReceived;

        /// <summary>
        /// Event that an "error no such server" message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> ErrNoSuchServerMessageReceived;

        /// <summary>
        /// Event that an "error no such Channel" message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> ErrNoSuchChannelMessageReceived;

        /// <summary>
        /// Event that an "error cannot send to channel" message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> ErrCannotSendToChanMessageReceived;

        /// <summary>
        /// Event that an "error no motd" message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> ErrNoMotdMessageReceived;

        /// <summary>
        /// Event that an info message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> InfoMessageReceived;

        /// <summary>
        /// Event that a member list message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> MemberListMessageReceived;

        /// <summary>
        /// Event that a more message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> ModeMessageReceived;

        /// <summary>
        /// Event that a privmsg message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> PrivmsgMessageReceived;

        /// <summary>
        /// Event that a join message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> JoinMessageReceived;

        /// <summary>
        /// Event that a part message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> PartMessageReceived;

        /// <summary>
        /// Event that a kick message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> KickMessageReceived;

        /// <summary>
        /// Event that an unknown message was received.
        /// </summary>
        public event EventHandler<IrcMessageEventArgs> UnknownMessageReceived;

        #endregion

        #endregion

        #region Properties

        /// <summary>
        /// Gets the store of all status messages
        /// </summary>
        public MessageStore StatusMessages { get; private set; }

        /// <summary>
        /// Gets or sets the configuration that is important for a IRC client.
        /// </summary>
        public ClientConfig Configuration
        {
            get { return _configuration; }
            set
            {
                _configuration = value;
                OnPropertyChanged(this, new PropertyChangedEventArgs("Configuration"));
                _configuration.PropertyChanged += (sender, e) => OnPropertyChanged(this, new PropertyChangedEventArgs("Configuration"));
            }
        }
        private ClientConfig _configuration; 

        /// <summary>
        /// Gets the dictionary of channels.
        /// Key: Name of channel, Value: IrcChannel object.
        /// </summary>
        public Dictionary<string, IrcChannel> Channels { get; private set; }

        /// <summary>
        /// Gets the storage for messages in a Dictionary
        /// </summary>
        public Dictionary<string, MessageStore> MessageStores { get; private set; }

        /// <summary>
        /// Gets the IrcConnection used by the client.
        /// </summary>
        public IrcConnection Connection { get; private set; }

        /// <summary>
        /// Gets a value indicating whether the IrcClient is connected at the moment.
        /// </summary>
        public bool Connected => Connection?.Connected ?? false;

        #endregion

        #region Public Methods

        /// <summary>
        /// Dispose this object
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Handler for new incomming Irc Messages
        /// </summary>
        /// <param name="sender">Sender of the message.</param>
        /// <param name="e">Event args including IRC message.</param>
        public void HandleIrcMessage(object sender, IrcMessageEventArgs e)
        {
            // validate
            if (sender == null) throw new ArgumentNullException(nameof(sender));
            if (e == null) throw new ArgumentNullException(nameof(e));

            EvaluateMessage(e.Message);
        }

        /// <summary>
        /// Evaluate a given IRCMessage and trigger Events
        /// </summary>
        /// <param name="message">The IRCMessage to evaluate</param>
        public void EvaluateMessage(IrcMessage message)
        {
            // validate
            if (message == null) throw new ArgumentNullException(nameof(message));

            switch (message.Command)
            {
                case "PING":
                    // PING Request
                    OnPingMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "NOTICE":
                    // We received a Notice
                    OnNoticeMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "401":
                    // ERR_NOSUCHNICK
                    OnErrNoSuchNickMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "402":
                    // ERR_NOSUCHSERVER
                    OnErrNoSuchServerMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "403":
                    // ERR_NOSUCHCHANNEL
                    OnErrNoSuchChannelMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "404": // ERR_CANNOTSENDTOCHAN
                    OnErrCannotSendToChanMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "422": // ERR_NOMOTD
                    OnErrNoMotdMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "001":
                case "002":
                case "003":
                case "004":
                case "005":
                case "251":
                case "254":
                case "255":
                case "256":
                case "265":
                case "266":
                    // Information only
                    OnInfoMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "353": // List of Members in a Channel
                    OnMemberListMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "MODE":
                    OnModeMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "JOIN":
                    OnJoinMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "PART":
                    OnPartMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "PRIVMSG":
                    OnPrivmsgMessageReceived(new IrcMessageEventArgs(message));
                    break;

                case "KICK":
                    OnKickMessageReceived(new IrcMessageEventArgs(message));
                    break;

                default:
                    OnUnknownMessageReceived(new IrcMessageEventArgs(message));
                    break;
            }
        }

        /// <summary>
        /// Connect to the ServerConfig
        /// </summary>
        public void Connect()
        {
            Connection = new IrcConnection(Configuration.ServerConfig);
            Connection.IrcMessageReceived += HandleIrcMessage;
            StatusMessages.AddMessage(new DisplayMessage(null, string.Format(CultureInfo.CurrentCulture, "Connecting to {0}:{1}", Configuration.ServerConfig.HostName, Configuration.ServerConfig.Port)));
            try
            {
                Connection.Connect();
            }
            catch (Exception ex)
            {
                Connection.Dispose();
                StatusMessages.AddMessage(new DisplayMessage(null, String.Format(CultureInfo.CurrentCulture, "Unable to connect ({0})", ex.Message)));
            }

            // Send the command to register
            Send(String.Format(CultureInfo.InvariantCulture, IrcMessage.NickMessage, Configuration.ServerConfig.Nickname));
            Send(String.Format(CultureInfo.InvariantCulture, IrcMessage.UserMessage, Configuration.ServerConfig.UserName, "HostName", "Servername", Configuration.ServerConfig.FullName));
        }

        /// <summary>
        /// Join Events so that we are handling all Events.
        /// </summary>
        /// <param name="eventsTypes">Events that should be handled automatically.</param>
        public void JoinEvents(IrcClientEventTypes eventsTypes)
        {
            // Check if we should handle Event Messages
            if (eventsTypes.HasFlag(IrcClientEventTypes.Ping))
            {
                // Add internal PingMessageHandler to PingMessageReceived Event
                Logger.Info((int)IrcTraceId.Client, Properties.Resources.StringEventHandlerAdded, "PingMessageHandler");
                PingMessageReceived += PingMessageHandler;
            }

            // Check if we should handle Information Messages
            if (eventsTypes.HasFlag(IrcClientEventTypes.Information))
            {
                // Add internal InformationEventHandler to 
                Logger.Info((int)IrcTraceId.Client, Properties.Resources.StringEventHandlerAdded, "InformationEventeHandler");
                InfoMessageReceived += InformationEventHandler;
            }

            // Check if we should handle ERR_NOSUCHCHAN Messages
            if (eventsTypes.HasFlag(IrcClientEventTypes.ErrorNoSuchChannel))
            {
                // Add Internal Error Handler for ERR_NOSUCHCHAN
                Logger.Info((int)IrcTraceId.Client, Properties.Resources.StringEventHandlerAdded, "ErrorEventHandler: ERR_NOSUCHCHAN");
                ErrNoSuchChannelMessageReceived += ErrorEventHandler;
            }

            // Check if we should handle ERR_NOSUCHNICK Messages
            if (eventsTypes.HasFlag(IrcClientEventTypes.ErrorNoSuchNick))
            {
                // Add Internal Error Handler for ERR_NOSUCHNICK
                Logger.Info((int)IrcTraceId.Client, Properties.Resources.StringEventHandlerAdded, "ErrorEventHandler: ERR_NOSUCHNICK");
                ErrNoSuchNickMessageReceived += ErrorEventHandler;
            }

            // Check if we should handle ERR_NOSUCHSERVER Messages
            if (eventsTypes.HasFlag(IrcClientEventTypes.ErrorNoSuchNick))
            {
                // Add Internal Error Handler for ERR_NOSUCHSERVER
                Logger.Info((int)IrcTraceId.Client, Properties.Resources.StringEventHandlerAdded, "ErrorEventHandler: ERR_NOSUCHSERVER");
                ErrNoSuchServerMessageReceived += ErrorEventHandler;
            }

            // Check if we should handle NOTICE Message
            if (eventsTypes.HasFlag(IrcClientEventTypes.Notice))
            {
                // Add Internal Handler for NOTICE
                NoticeMessageReceived += InformationEventHandler;
            }

            // Check if we should handle JOIN Message
            if (eventsTypes.HasFlag(IrcClientEventTypes.Join))
            {
                // Add Internal Handler for JOIN
                JoinMessageReceived += JoinEventHandler;
            }

            // Check if we should handle PART Message
            if (eventsTypes.HasFlag(IrcClientEventTypes.Part))
            {
                // Add Internal Handler for PART
                PartMessageReceived += PartEventHandler;
            }

        }

        /// <summary>
        /// Send a message to the server
        /// </summary>
        /// <param name="message">message to send.</param>
        public void Send(string message)
        {
            if (Connected)
                Connection.SendMessage(new IrcMessage(message));
        }

        /// <summary>
        /// Send a message to the server
        /// </summary>
        /// <param name="message">message to send.</param>
        public void Send(IrcMessage message)
        {
            if (Connected)
                Connection.SendMessage(message);
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Send the NewChannelCreated event.
        /// </summary>
        /// <param name="e">ChannelEventArgs to send.</param>
        protected virtual void OnNewChannelCreated(ChannelEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = NewChannelCreated;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the PingMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnPingMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = PingMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the NoticeMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnNoticeMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = NoticeMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the ErrNoSuchNickMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnErrNoSuchNickMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = ErrNoSuchNickMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the ErrNoSuchServerMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnErrNoSuchServerMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = ErrNoSuchServerMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the ErrNoSuchChannelMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnErrNoSuchChannelMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = ErrNoSuchChannelMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the ErrCannotSendToChanMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnErrCannotSendToChanMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = ErrCannotSendToChanMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the ErrNoMotdMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnErrNoMotdMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = ErrNoMotdMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the InfoMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnInfoMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = InfoMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the MemberListMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnMemberListMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = MemberListMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the ModeMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnModeMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = ModeMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the PrivmsgMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnPrivmsgMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = PrivmsgMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the JoinMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnJoinMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = JoinMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the PartMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnPartMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = PartMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the KickMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnKickMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = KickMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        /// <summary>
        /// Send the UnknownMessageReceived event.
        /// </summary>
        /// <param name="e">IrcMessageEventArgs to send.</param>
        protected virtual void OnUnknownMessageReceived(IrcMessageEventArgs e)
        {
            // Local copy to prevent race conditions
            var handler = UnknownMessageReceived;

            // raise the event
            handler?.Invoke(this, e);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Remove all EventHandlers from all events.
        /// </summary>
        private void RemoveAllHandlers()
        {
            PingMessageReceived = null;
            NoticeMessageReceived = null;
            ErrNoSuchNickMessageReceived = null;
            ErrNoSuchServerMessageReceived = null;
            ErrNoSuchChannelMessageReceived = null;
            ErrCannotSendToChanMessageReceived = null;
            ErrNoMotdMessageReceived = null;
            InfoMessageReceived = null;
            MemberListMessageReceived = null;
            ModeMessageReceived = null;
            PrivmsgMessageReceived = null;
            JoinMessageReceived = null;
            PartMessageReceived = null;
            KickMessageReceived = null;
            UnknownMessageReceived = null;
        }

        /// <summary>
        /// Dispose this object
        /// </summary>
        /// <param name="disposing">Do all the disposing.</param>
        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!_disposed)
            {
                if (disposing)
                {
                    // Make sure we do not try to send any more events.
                    RemoveAllHandlers();

                    // Dispose Connection
                    if (Connection != null)
                    {
                        if (Connected)
                            Connection.Close();

                        Connection.Dispose();
                    }
                }

                Connection = null;
                _disposed = true;
            }
        }

        /// <summary>
        /// Handle Ping Events
        /// </summary>
        /// <param name="sender">The sender of the Event</param>
        /// <param name="e">The IRC Message with the PING command</param>
        private void PingMessageHandler(object sender, IrcMessageEventArgs e)
        {
            // Log information
            Logger.Info((int)IrcTraceId.Client, Properties.Resources.StringPingMessageHandlerSendingPong);

            // Show Ping in status window if configured
            if (Configuration.ShowPings)
                StatusMessages.AddMessage(new DisplayMessage(string.Empty, String.Format(CultureInfo.CurrentCulture, "Ping-Pong ({0})", e.Message.Parameters), IrcClientEventTypes.Ping));

            Connection.SendMessage(new IrcMessage(String.Format(CultureInfo.InvariantCulture, IrcMessage.PongMessage, e.Message.Parameter)));
        }

        /// <summary>
        /// Handle all Informational Messages
        /// </summary>
        /// <param name="sender">The sender of the Event</param>
        /// <param name="e">The IRC Message with the info command</param>
        private void InformationEventHandler(object sender, IrcMessageEventArgs e)
        {
            // Log information
            Logger.Info((int)IrcTraceId.Client, "Info Message arrived: {0}", e.Message);

            // Show Ping in status window if configured
            if (Configuration.ShowPings)
                StatusMessages.AddMessage(new DisplayMessage(e.Message.UserOfPrefix(), e.Message.VisualParameters));
        }

        /// <summary>
        /// Handle all error messages.
        /// </summary>
        /// <param name="sender">The sender of the Event</param>
        /// <param name="e">The IRC Message with the info command</param>
        private void ErrorEventHandler(object sender, IrcMessageEventArgs e)
        {
            // TODO
        }

        /// <summary>
        /// Handle join messages.
        /// </summary>
        /// <param name="sender">The sender of the Event</param>
        /// <param name="e">The IRC Message with the info command</param>
        private void JoinEventHandler(object sender, IrcMessageEventArgs e)
        {
            // Validation of Parameters
            if (e.Message.Parameter.Count == 0)
            {
                // Invalid message. We need first argument as channel name.
                return;
            }

            // Validation of who joined.
            string who = e.Message.UserOfPrefix();
            if (String.IsNullOrWhiteSpace(who))
            {
                // No nickname given!
                return;
            }

            // Do the join.
            var channelName = e.Message.Parameter[0];
            if (!Channels.ContainsKey(channelName))
            {
                var newChannel = new IrcChannel(this, channelName);
                Channels.Add(channelName, newChannel);
                OnNewChannelCreated(new ChannelEventArgs(newChannel));
            }

            var channel = Channels[channelName];
            channel.InChannel = true;
            channel.AddUser(who);
        }

        /// <summary>
        ///  Handle part messages.
        /// </summary>
        /// <param name="sender">The sender of the Event</param>
        /// <param name="e">The IRC Message with the info command</param>
        private void PartEventHandler(object sender, IrcMessageEventArgs e)
        {
            // Validation of Parameters
            if (e.Message.Parameter.Count == 0)
            {
                // Invalid message. We need first argument as channel name.
                return;
            }

            // Validation of who parted.
            var who = e.Message.UserOfPrefix();
            if (String.IsNullOrWhiteSpace(who))
            {
                // No nickname given!
                return;
            }

            // Do the part.
            var channelName = e.Message.Parameter[0];
            if (Channels.ContainsKey(channelName))
            {
                var channel = Channels[channelName];
                channel.RemoveUser(who);
            }
        }

        #endregion

        #region INotifyPropertyChanged implementation

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // prevent racing conditions
            var handler = PropertyChanged;

            // trigger event
            handler?.Invoke(sender, e);
        }

        #endregion
    }
}
